Skip to content

Conversation

@MatthewBarghout
Copy link
Collaborator

High-level motivation for making the changes
Added server-side pagination, sorting, and filtering to the /api/parties/ endpoint to improve performance and scalability as the database grows. Previously, all filtering and sorting happened client-side, which doesn't scale beyond a few hundred records.
Changes:
Core utilities (backend/src/core/query_utils.py):

Added reusable pagination, sorting, and filtering utilities for SQLAlchemy queries
Implemented PaginationParams, SortParam, FilterParam models with Pydantic validation
Created apply_query_params() to combine filters, sorting, and pagination with security whitelisting
Added get_total_count() to efficiently count filtered results before pagination

Party service (backend/src/modules/party/party_service.py):

Added get_parties_paginated() method with support for:

Optional pagination (page_number, page_size)
Sorting by party_datetime, location_id, contact_one_id, or id
Filtering by location_id and/or contact_one_id

Whitelisted allowed sort/filter fields for security

Party router (backend/src/modules/party/party_router.py):

Updated list_parties endpoint to accept query parameters for pagination, sorting, and filtering
All features are opt-in with sensible defaults (backward compatible)

Tests (backend/test/modules/party/test_party_list_features.py):

Added comprehensive test suite with 22 tests covering:

Pagination edge cases (empty DB, exact pages, remainders, beyond last page)
Sorting (ascending/descending by different fields)
Filtering (single and multiple filters)
Combined features (filter + sort + paginate together)

Bug fixes:

Fixed PartyTestUtils.next_dict() to avoid mutating the overrides dict (was causing test failures)
Fixed get_total_count() to properly count rows instead of columns

Closes #161

@github-actions
Copy link

Test Results Summary

302 tests  ±0   302 ✅ ±0   11s ⏱️ ±0s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit b10ba3f. ± Comparison against base commit c972c7c.

@MatthewBarghout MatthewBarghout changed the title fixes BackendFilter/Sort Jan 29, 2026
Copy link
Collaborator

@naasanov naasanov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple requests involving centralizing logic more and extending to the rest of the modules

Comment on lines +289 to +306
class PaginatedResponse[ModelType](BaseModel):
"""Generic paginated response wrapper matching existing PaginatedResponse."""

items: list[ModelType]
total_records: int
page_size: int
page_number: int
total_pages: int

@property
def has_next(self) -> bool:
"""Check if there's a next page."""
return self.page_number < self.total_pages

@property
def has_prev(self) -> bool:
"""Check if there's a previous page."""
return self.page_number > 1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now duplicated between core/models.py

Comment on lines +192 to +203

Args:
page_number: Page number (1-indexed)
page_size: Items per page (None = all items)
sort_by: Field to sort by
sort_order: Sort order ('asc' or 'desc')
location_id: Filter by location ID
contact_one_id: Filter by contact one (student) ID

Returns:
PaginatedPartiesResponse with items and metadata
"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still a pretty long function. Would be great to have a util function that combines these all together since it will look very similar between all the "get all" routes

Comment on lines +187 to +188
location_id: int | None = None,
contact_one_id: int | None = None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit restrictive. These routes will be used to replace the filter in the admin table, which means the function should be able to filter by each column. To acheive this without the function becoming too bulky I'm cool with just extracting the raw dict of query params and then validating them against the allowed fields within the util function

Comment on lines 383 to 395
"""Create a party registration from an admin. Both contacts must be specified."""
# Get/create location and validate no hold
location = await self._validate_and_get_location(dto.google_place_id)

# Get contact_one by email
contact_one = await self._get_student_by_email(dto.contact_one_email)

# Create party data with contact_two information directly
party_data = PartyData(
party_datetime=dto.party_datetime,
location_id=location.id,
contact_one_id=contact_one.account_id,
contact_two=dto.contact_two,
)

# Create party
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer having the whitespace here because it makes it more readable, and format changes are outside the scope of the ticket

Comment on lines +76 to +87
# Create a copy to avoid mutating the original
local_overrides = dict(overrides)

if "location_id" not in local_overrides:
location = await self.location_utils.create_one()
overrides["location_id"] = location.id
local_overrides["location_id"] = location.id

if "contact_one_id" not in overrides:
if "contact_one_id" not in local_overrides:
student = await self.student_utils.create_one()
overrides["contact_one_id"] = student.account_id
local_overrides["contact_one_id"] = student.account_id

return await super().next_dict(**overrides)
return await super().next_dict(**local_overrides)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good addition. In order to maintain consistency, make these changes in all functions that use overrides across all the util classes

Comment on lines +27 to +43
@pytest.mark.asyncio
async def test_debug_party_creation(self):
"""Debug test to see what's happening with party creation."""
print("\n=== Starting test ===")

for i in range(5):
print(f"\n--- Creating party {i + 1} ---")
party = await self.party_utils.create_one()
print(f"Created party with ID: {party.id}")
print(f"Location ID: {party.location_id}, Contact ID: {party.contact_one_id}")

print("\n=== Checking database ===")
response = await self.admin_client.get("/api/parties/")
data = response.json()
print(f"Total records from API: {data['total_records']}")
print(f"Number of items: {len(data['items'])}")
print(f"Item IDs: {[item['id'] for item in data['items']]}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This debug test should be removed for main

Comment on lines +1 to +5
"""
Tests for party list endpoint with pagination, sorting, and filtering.

Add these tests to your existing party_router_test.py file or create a new test file.
"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since testing pagination and filtering and sorting will result in a lot of bloat across tests, it would be a good idea to make utils that we can reuse to test get all routes across routers. This could be done using parameterized tests, test generators (like in test_templates.py), util functions (like in assertions.py), or mix of all of them.

parties = result.scalars().all()
return [party.to_dto() for party in parties]

async def get_parties_paginated(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need filter/sort/pagination on all "get all" routes. If the logic is properly centralized within the utils function it should just require <=10 lines of configuration for each function

)

# Build query params
filters: list[FilterParam] = []
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have an overarching filter function, we can make "get parties by (location/contact/date_range/etc.)" delegate to get_parties_paginated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Backend FIlter/Sort

3 participants